在先前我們為了讓查詢使用上更加彈性,學習了使用變數(Variables)動態帶入引數(Arguments),接著我們學習指令(Directives),它就像是 GraphQL 中的魔法咒語一般,讓我們可以動態地改變查詢結果,像是動態改變查詢結果格式、改變查詢結果資料樣式或是一定程度上共用查詢語法。
指令(Directives)語法是@
加上名稱,後面的括號內是帶入的引數與引數值,如下圖:
query UserQuery(
$userId: Int!,
$hideRole: Boolean! = false,
$includeDetail: Boolean! = true
) {
user(id: $userId) {
username
id
email
isActive
fullName
role @skip(if: $hideRole)
detail @include(if: $includeDetail) {
... on UserProfile {
address
}
}
}
}
{
"userId": {自行帶入可用的ID},
"hideRole": true,
"includeDetail": false
}
這邊新定義了兩個不可是 Null 的 Boolean 變數$hideRole
與$includeDetail
,兩個變數都給了預設值,detail 內的 內嵌片段(Inline Fragments) 是一個UserProfile
的介面(Interface),然後我們可以看到兩個指令:
@skip(if: Boolean!)
:如果符合條件就跳過該欄位或物件。@include(if: Boolean!)
:如果符合條件就包含該欄位或物件。以上這兩個指令是 GraphQL 預設內建的指令,接著我們使用 Strawberry 自定義自己的指令。
# app/__init__.py
import strawberry
+from graphql import DirectiveLocation
from app import query
__all__ = ["schema"]
+@strawberry.directive(
+ locations=[DirectiveLocation.FIELD],
+ name="sensitive",
+ description="Replace sensitive text with *",
+)
+def sensitive_text(value: str) -> str:
+ ignore_list = [" ", ",", ",", ".", "。", "!", "?", "!", "?"]
+ return "".join([i if i in ignore_list else "*" for i in value])
-schema = strawberry.Schema(query=query.Query)
+schema = strawberry.Schema(query=query.Query, directives=[sensitive_text])
query UserQuery(
$userId: Int!,
$hideRole: Boolean! = false,
$includeDetail: Boolean! = true
) {
user(id: $userId) {
username
id
email
isActive
fullName @sensitive
role @skip(if: $hideRole)
detail @include(if: $includeDetail) {
... on UserProfile {
address
phone @sensitive
birthdate
}
}
}
}
上面我們實作了一個名為@sensitive
的指令用於讓客戶端可以自行決定哪些欄位是敏感文字,將結果的文字轉成*
,在程式碼的部分我們定義一個函式sensitive_text
,在 Strawberry 預設的情況下會使用函式名稱轉成駝峰式命名當成指令名稱,但是我們在@strawberry.directive
的地方設定了名稱,並且設定作用位置是欄位。在函式內先定義一個忽略轉換的文字清單,最後一個字一個字檢查,不在忽略清單的文字就轉成*
,@sensitive
是一個不需要引數的指令。
接著我們再練習一個帶有引數的指令:
# app/__init__.py
# ... 省略
+@strawberry.directive(locations=[DirectiveLocation.FIELD])
+def replace(value: str, old: str, new: str):
+ return value.replace(old, new)
-schema = strawberry.Schema(query=query.Query, directives=[sensitive_text])
+schema = strawberry.Schema(
+ query=query.Query,
+ directives=[sensitive_text, replace],
+)
query UserQuery($userId: Int!) {
user(id: $userId) {
username
id
email
isActive
fullName @sensitive @replace(old: "*", new: "#")
role
detail {
... on UserProfile {
address
phone @sensitive
birthdate
}
}
}
}
這邊我們新增了一個字串替換的指令,在字串欄位使用@replace(old: String, new: String)
,然後可以看到指令是可以多個一起使用,就如同 Linux 的 pipe 一樣,但是相同的指令無法在同一個位置重複使用,且指令的回傳型態要與原本相同。
在前面提到的指令(Directives)其實全名叫 操作指令(Operation Directives) ,顧名思義就是會影響執行過程的指令,會這麼說是因為還有另ㄧ種類型的指令,叫做 Schema 指令(Schema Directives) ,它就是在前面說明型別系統時,所提到的 Directive,主要功能是提供 Schema 額外資訊(Metadata)的描述。
接著來練習 Schema 指令,我們先幫 User 多加個欄位:
# app/types.py
# ... 省略
from strawberry import scalars
# ... 省略
@strawberry.type
class User:
id: strawberry.ID
username: str
email: str
first_name: str
last_name: str
password: str
+ avatar: typing.Optional[scalars.Base64] = strawberry.field(
+ description="Base64 encoded avatar",
+ deprecation_reason="Removed this field.",
+ default=None,
+ )
# ... 省略
接著我們將 GraphQL Schema 匯出成檔案:
$ strawberry export-schema main:schema --output schema.graphql
Schema exported to schema.graphql
然後我們打開schema.graphql
檔案來看,可以看到以下兩個特別的標註:
"""
Represents binary data as Base64-encoded strings, using the standard alphabet.
"""
scalar Base64 @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc4648.html#section-4")
type User {
id: ID!
username: String!
email: String!
firstName: String!
lastName: String!
password: String!
"""Base64 encoded avatar"""
avatar: Base64 @deprecated(reason: "Removed this field.")
lastLogin: DateTime
"""Is the user active?"""
isActive: Boolean!
role: UserRole!
detail: UserDetail!
fullName: String!
}
可以看到兩個 Schema 指令@specifiedBy
跟@deprecated
,這兩個指令都是內建指令:
@specifiedBy
:用於標記客製化 Scalar 規格文件 URL。@deprecated
:用於標記棄用與棄用說明的欄位或是 Enum 值。strawberry.scalars.Base64
是 strawberry 幫我們預先建立好的客製化 Scalar。
Schema 指令 strawberry 也有提供自行客製化的功能,如有相關需求可以參考官方文件說明 [1]。
app/__init__.py
完整程式碼:
import datetime
import typing
import strawberry
from graphql import DirectiveLocation
from app import query
__all__ = ["schema"]
@strawberry.directive(
locations=[DirectiveLocation.FIELD],
name="sensitive",
description="Replace sensitive text with *",
)
def sensitive_text(value: str) -> str:
ignore_list = [" ", ",", ",", ".", "。", "!", "?", "!", "?"]
return "".join([i if i in ignore_list else "*" for i in value])
@strawberry.directive(locations=[DirectiveLocation.FIELD])
def replace(value: str, old: str, new: str):
return value.replace(old, new)
schema = strawberry.Schema(
query=query.Query,
directives=[sensitive_text, replace],
)
app/types.py
完整程式碼:
import datetime
import enum
import typing
import strawberry
from strawberry import scalars
@strawberry.enum
class UserRole(enum.Enum):
NORMAL = strawberry.enum_value(
"normal",
description="Normal user",
)
STAFF = "staff"
MANAGER = "manager"
ADMIN = "admin"
@strawberry.interface
class UserProfile:
phone: str
birthdate: datetime.date
address: typing.Optional[str]
@strawberry.type
class NormalUserDetail(UserProfile):
pass
@strawberry.type
class StaffUserDetail(UserProfile):
department: str
@strawberry.type
class ManagerUserDetail(StaffUserDetail):
subordinates: typing.List["User"]
@strawberry.type
class AdminUserDetail:
system_permissions: typing.List[str]
@strawberry.type
class User:
id: strawberry.ID
username: str
email: str
first_name: str
last_name: str
password: str
avatar: typing.Optional[scalars.Base64] = strawberry.field(
description="Base64 encoded avatar",
deprecation_reason="Removed this field.",
default=None,
)
last_login: typing.Optional[datetime.datetime]
is_active: bool = strawberry.field(
default=True,
description="Is the user active?",
)
role: UserRole
detail: typing.Annotated[
typing.Union[
NormalUserDetail,
StaffUserDetail,
ManagerUserDetail,
AdminUserDetail,
],
strawberry.union("UserDetail"),
]
@strawberry.field
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"